package online.heycm.platform.oss;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import com.amazonaws.services.s3.transfer.Download;
import com.amazonaws.services.s3.transfer.MultipleFileUpload;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import lombok.extern.slf4j.Slf4j;
import online.heycm.platform.common.entity.exception.BizException;
import online.heycm.platform.oss.functional.S3Operate;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

@Slf4j
public class OssServiceImpl implements OssService {

    private final AmazonS3 amazonS3;

    private final TransferManager transferManager;

    private final String bucketName;

    public OssServiceImpl(AmazonS3 amazonS3, String bucketName) {
        this.amazonS3 = amazonS3;
        this.bucketName = bucketName;
        this.transferManager = new TransferManager(amazonS3);
        this.createBucket();
    }

    @Override
    public boolean isBucketExist() {
        return operateBool(() -> amazonS3.doesBucketExistV2(bucketName));
    }

    @Override
    public boolean createBucket() {
        return operateBool(() -> {
            if (this.isBucketExist()) {
                log.info("Bucket {} already exists.", bucketName);
                return true;
            }
            CreateBucketRequest request = new CreateBucketRequest(bucketName);
            Bucket bucket = amazonS3.createBucket(request);
            log.info("{} created.", bucket);
            return true;
        });
    }

    @Override
    public boolean deleteBucket() {
        return operateBool(() -> {
            amazonS3.deleteBucket(bucketName);
            return true;
        });
    }

    @Override
    public Bucket getBucket() {
        return operate(() -> {
            List<Bucket> buckets = amazonS3.listBuckets();
            return buckets.stream().filter(bucket -> bucket.getName().equals(bucketName)).findFirst().orElse(null);
        });
    }

    @Override
    public List<Bucket> listBuckets() {
        return operate(amazonS3::listBuckets);
    }

    @Override
    public boolean setBucketAcl(AccessControlList acl) {
        return operateBool(() -> {
            amazonS3.setBucketAcl(bucketName, acl);
            return true;
        });
    }

    @Override
    public AccessControlList getBucketAcl() {
        return operate(() -> amazonS3.getBucketAcl(bucketName));
    }

    @Override
    public boolean putObject(String objectName, File file) {
        return putObject(objectName, file, null);
    }

    @Override
    public boolean putObject(String objectName, File file, Map<String, String> metadata) {
        try {
            String contentType = Files.probeContentType(file.toPath());
            return putObject(objectName, file, contentType, metadata);
        } catch (IOException e) {
            log.error("S3 operation error.", e);
        }
        return false;
    }

    @Override
    public boolean putObject(String objectName, File file, String contentType, Map<String, String> metadata) {
        return operateBool(() -> {
            ObjectMetadata om = new ObjectMetadata();
            om.setContentType(contentType);
            om.setContentLength(file.length());
            om.setCacheControl("no-cache");
            om.setUserMetadata(metadata);
            PutObjectRequest request = new PutObjectRequest(bucketName, objectName, file);
            request.setMetadata(om);
            // amazonS3.putObject(request);
            Upload upload = transferManager.upload(request);
            try {
                upload.waitForUploadResult();
            } catch (InterruptedException e) {
                log.error("S3 operation error.", e);
                return false;
            }
            return true;
        });
    }

    @Override
    public boolean putObject(String objectName, InputStream inputStream) {
        return putObject(objectName, inputStream, null);
    }

    @Override
    public boolean putObject(String objectName, InputStream inputStream, Map<String, String> metadata) {
        return putObject(objectName, inputStream, "application/octet-stream", metadata);
    }

    @Override
    public boolean putObject(String objectName, InputStream inputStream, String contentType, Map<String, String> metadata) {
        long length;
        try {
            length = inputStream.available();
        } catch (IOException e) {
            log.error("S3 operation error.", e);
            return false;
        }
        return operateBool(() -> {
            ObjectMetadata om = new ObjectMetadata();
            om.setContentType(contentType);
            om.setContentLength(length);
            om.setCacheControl("no-cache");
            om.setUserMetadata(metadata);
            // amazonS3.putObject(bucketName, objectName, inputStream, om);
            Upload upload = transferManager.upload(bucketName, objectName, inputStream, om);
            try {
                upload.waitForUploadResult();
            } catch (InterruptedException e) {
                log.error("S3 operation error.", e);
                return false;
            }
            return true;
        });
    }

    @Override
    public S3Object getObject(String objectName) {
        return operate(() -> amazonS3.getObject(bucketName, objectName));
    }

    @Override
    public boolean deleteObject(String objectName) {
        return operateBool(() -> {
            amazonS3.deleteObject(bucketName, objectName);
            return true;
        });
    }

    @Override
    public boolean isObjectExist(String objectName) {
        return operateBool(() -> amazonS3.doesObjectExist(bucketName, objectName));
    }

    @Override
    public ObjectMetadata getObjectMetadata(String objectName) {
        return operate(() -> amazonS3.getObjectMetadata(bucketName, objectName));
    }

    @Override
    public boolean copyObject(String sourceObjectName, String targetObjectName) {
        return operateBool(() -> {
            amazonS3.copyObject(bucketName, sourceObjectName, bucketName, targetObjectName);
            return true;
        });
    }

    @Override
    public List<String> listObject() {
        return listObject(Integer.MAX_VALUE);
    }

    @Override
    public List<String> listObject(int maximum) {
        return listObject(null, maximum);
    }

    @Override
    public List<String> listObject(String prefix) {
        return listObject(prefix, Integer.MAX_VALUE);
    }

    @Override
    public List<String> listObject(String prefix, int maximum) {
        if (maximum <= 0) {
            throw new BizException("S3 operate listObject maximum must be greater than 0.");
        }
        return operate(() -> {
            List<String> list = new ArrayList<>();
            int count = 0;
            ListObjectsV2Request request = new ListObjectsV2Request();
            request.setBucketName(bucketName);
            request.setPrefix(prefix);
            request.setMaxKeys(100);
            while (true) {
                ListObjectsV2Result result = amazonS3.listObjectsV2(request);
                for (S3ObjectSummary objectSummary : result.getObjectSummaries()) {
                    list.add(objectSummary.getKey());
                    if (++count >= maximum) {
                        return list;
                    }
                }
                if (!result.isTruncated()) {
                    break;
                }
                request.setContinuationToken(result.getNextContinuationToken());
            }
            return list;
        });
    }

    @Override
    public String getPresignedUrl(String objectName) {
        return getPresignedUrl(objectName, 60 * 60 * 24 * 7);
    }

    @Override
    public String getPresignedUrl(String objectName, int seconds) {
        return getPresignedUrl(objectName, seconds, HttpMethod.GET);
    }

    @Override
    public String getPresignedUrl(String objectName, int seconds, HttpMethod method) {
        return operate(() -> {
            GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectName);
            request.setMethod(method);
            request.setExpiration(new Date(System.currentTimeMillis() + seconds * 1000L));
            return amazonS3.generatePresignedUrl(request).toString();
        });
    }

    @Override
    public String getUrl(String objectName) {
        return operate(() -> amazonS3.getUrl(bucketName, objectName).toString());
    }

    @Override
    public String getPutUrl(String objectName) {
        return getPutUrl(objectName, 60 * 60 * 24 * 7);
    }

    @Override
    public String getPutUrl(String objectName, int seconds) {
        return getPresignedUrl(objectName, seconds, HttpMethod.PUT);
    }

    @Override
    public boolean uploadDirectory(String objectNamePrefix, File file) {
        return operateBool(() -> {
            MultipleFileUpload upload = transferManager.uploadDirectory(bucketName, objectNamePrefix, file, false);
            try {
                upload.waitForCompletion();
            } catch (InterruptedException e) {
                log.error("S3 operation error.", e);
                return false;
            }
            return true;
        });
    }

    @Override
    public boolean uploadDeepDirectory(String objectNamePrefix, File file) {
        return operateBool(() -> {
            MultipleFileUpload upload = transferManager.uploadDirectory(bucketName, objectNamePrefix, file, true);
            try {
                upload.waitForCompletion();
            } catch (InterruptedException e) {
                log.error("S3 operation error.", e);
                return false;
            }
            return true;
        });
    }

    @Override
    public boolean download(String objectName, File file) {
        return operateBool(() -> {
            Download download = transferManager.download(bucketName, objectName, file);
            try {
                download.waitForCompletion();
            } catch (InterruptedException e) {
                log.error("S3 operation error.", e);
                return false;
            }
            return true;
        });
    }

    private <T> T operate(S3Operate<T> operate) {
        try {
            return operate.operate();
        } catch (Exception e) {
            log.error("S3 operation error.", e);
        }
        return null;
    }

    private boolean operateBool(S3Operate<Boolean> operate) {
        try {
            return operate.operate();
        } catch (Exception e) {
            log.error("S3 operation error.", e);
        }
        return false;
    }
}
